import numpy as np
# 步骤二：神经网络类的设计
class NeuralNetwork:
    """
        定义我们的神经网络模型。
    """
    def __init__(self,input_nodes,hidden_nodes,output_nodes,learning_rate):
        """
       初始化神经网络的参数。
        - input_nodes: 输入层节点数 (对于MNIST是 28*28=784)
        - hidden_nodes: 隐藏层节点数 (这是一个超参数，可以调整，我们先设为200)
        - output_nodes: 输出层节点数 (对于0-9的数字分类，是10)
        - learning_rate: 学习率，控制每次更新权重时的步长
        """
        self.inodes = input_nodes
        self.hnodes = hidden_nodes
        self.onodes = output_nodes
        self.lr = learning_rate

        # 权重初始化 ---
        # 权重是神经网络需要学习的东西。我们不能把它们都初始化为0，
        # 否则网络就学不到东西了。一个好的实践是根据节点数的平方根来初始化，
        # 这样可以保证信号在前向传播时不会过大或过小。

        # wih: a weight matrix for the links between the input and hidden layers
        # who: a weight matrix for the links between the hidden and output layers
        self.wih = np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
        self.who = np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))

        # --- 激活函数 ---
        # 我们使用Sigmoid函数，它能将任意数值映射到(0, 1)之间，
        # 非常适合用来模拟神经元的激活状态。
        self.activation_function = self._sigmoid
    
    def _sigmoid(self, x):
        """Sigmoid激活函数"""
        return 1 / (1 + np.exp(-x))

    def train(self, inputs_list, targets_list):
        """
        训练神经网络。
        - inputs_list: 输入数据，一个样本的特征向量
        - targets_list: 目标输出，对应的one-hot编码的标签
        """
        # 将输入列表转换为二维数组（列向量）
        inputs = np.array(inputs_list, ndmin=2).T
        targets = np.array(targets_list, ndmin=2).T
        
        # 前向传播 - 计算隐藏层的输入和输出
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        
        # 前向传播 - 计算输出层的输入和输出
        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)
        
        # 计算误差
        output_errors = targets - final_outputs
        
        # 反向传播误差到隐藏层
        hidden_errors = np.dot(self.who.T, output_errors)
        
        # 更新权重
        # 输出层权重更新
        self.who += self.lr * np.dot((output_errors * final_outputs * (1.0 - final_outputs)),hidden_outputs.T)
        
        # 隐藏层权重更新
        self.wih += self.lr * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)),inputs.T)
        
        return
    
    def query(self, inputs_list):
        """
        使用训练好的神经网络进行预测。
        - inputs_list: 输入数据，一个样本的特征向量
        返回: 预测的输出向量
        """
        # 将输入列表转换为二维数组（列向量）
        inputs = np.array(inputs_list, ndmin=2).T
        
        # 前向传播 - 计算隐藏层的输入和输出
        hidden_inputs = np.dot(self.wih, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        
        # 前向传播 - 计算输出层的输入和输出
        final_inputs = np.dot(self.who, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)
        
        return final_outputs
    
    def evaluate(self, test_data, test_labels):
        """
        评估神经网络在测试集上的性能。
        - test_data: 测试数据集
        - test_labels: 测试数据集的标签
        返回: 准确率
        """
        # 记录正确预测的数量
        correct = 0
        
        # 遍历测试数据集
        for i in range(len(test_data)):
            # 获取网络的预测结果
            outputs = self.query(test_data[i])
            
            # 找出预测概率最高的类别
            predicted_label = np.argmax(outputs)
            
            # 如果预测正确，增加计数
            if predicted_label == test_labels[i]:
                correct += 1
        
        # 计算准确率
        accuracy = correct / len(test_data)
        return accuracy


